package com.cicadasmall.system.service.impl;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.cicadasmall.common.constant.Constant;
import com.cicadasmall.common.exception.ServiceException;
import com.cicadasmall.security.support.ClientsUtils;
import com.cicadasmall.system.connect.mobile.MobileAuthToken;
import com.cicadasmall.system.connect.wxapp.WxMaAuthToken;
import com.cicadasmall.system.connect.wxmp.WxMpAuthToken;
import com.cicadasmall.security.LoginUserDetails;
import com.cicadasmall.system.service.ITokenService;
import com.cicadasmall.system.vo.TokenVO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.common.exceptions.UnsupportedGrantTypeException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.stereotype.Service;

import java.util.*;


@Service
public class TokenServiceImpl implements ITokenService {

    @Autowired
    private ClientDetailsService redisClientDetailsService;

    @Autowired
    private RandomValueAuthorizationCodeServices redisAuthorizationCodeServices;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;


    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    public Page<TokenVO> getTokenList(int current, int size) {
        try {
            List<TokenVO> list = new ArrayList<>();
            // Set<String> keys = Optional.ofNullable(redisTemplate.keys("access:" +
            // "*")).orElse(Sets.newHashSet(""));
            // 根据分页参数获取对应数据
            List<String> keys = findKeysForPage(Constant.PREFIX + Constant.OAUTH_PREFIX + "access:" + "*", current, size);

            for (Object key : keys.toArray()) {
                // String key = page;
                // String accessToken = StringUtils.substringAfter(key, "access:");
                // OAuth2AccessToken token =
                // tokenStore.readAccessToken(accessToken);
                OAuth2AccessToken token = (OAuth2AccessToken) redisTemplate.opsForValue().get(key);
                TokenVO tokenVO = new TokenVO();


                if (token != null) {
                    tokenVO.setTokenType(token.getTokenType());
                    tokenVO.setTokenValue(token.getValue());
                    tokenVO.setExpiresIn(token.getExpiresIn());
                }
                OAuth2Authentication oAuth2Auth = tokenStore.readAuthentication(token);
                Authentication authentication = oAuth2Auth.getUserAuthentication();
                tokenVO.setClientId(oAuth2Auth.getOAuth2Request().getClientId());
                tokenVO.setGrantType(oAuth2Auth.getOAuth2Request().getGrantType());

                if (authentication instanceof UsernamePasswordAuthenticationToken) {
                    UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;

                    if (authenticationToken.getPrincipal() instanceof LoginUserDetails) {
                        LoginUserDetails user = (LoginUserDetails) authenticationToken.getPrincipal();
                        tokenVO.setUid(user.getUid());
                        tokenVO.setUsername(user.getUsername());
                        tokenVO.setAvatar(user.getAvatar());
                    }

                } else if (authentication instanceof PreAuthenticatedAuthenticationToken) {
                    // 刷新token方式
                    PreAuthenticatedAuthenticationToken authenticationToken = (PreAuthenticatedAuthenticationToken) authentication;
                    if (authenticationToken.getPrincipal() instanceof LoginUserDetails) {
                        LoginUserDetails user = (LoginUserDetails) authenticationToken.getPrincipal();
                        tokenVO.setUid(user.getUid());
                        tokenVO.setUsername(user.getUsername());
                        tokenVO.setAvatar(user.getAvatar());
                    }

                }
                list.add(tokenVO);
            }
            Page<TokenVO> page = new Page(current, size, keys.size());
            page.setRecords(list);
            return page;
        } catch (Exception e) {
            throw new ServiceException(e.getMessage());
        }
    }

    @Override
    public OAuth2AccessToken passwordToken(String header, String grantType, String username, String password) {
        ClientsUtils.ClientInfo client = ClientsUtils.buildClientInfo(header);
        if (!StrUtil.equals(grantType, "password")) {
            throw new UnsupportedGrantTypeException("grant_type 必须为password");
        }
        ClientDetails clientDetails = ClientsUtils.getClientDetails(client.getClientId(), client.getClientSecret());
        TokenRequest tokenRequest = new TokenRequest(org.apache.commons.collections4.MapUtils.EMPTY_SORTED_MAP, client.getClientId(), clientDetails.getScope(),
                grantType);
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
        OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
        oAuth2Authentication.setAuthenticated(true);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken mobileToken(String header, String grantType, String deviceId, String mobile, String code) {
        ClientsUtils.ClientInfo client = ClientsUtils.buildClientInfo(header);
        if (!StrUtil.equals(grantType, "mobile")) {
            throw new UnsupportedGrantTypeException("grant_type 必须为mobile");
        }
        ClientDetails clientDetails = ClientsUtils.getClientDetails(client.getClientId(), client.getClientSecret());
        TokenRequest tokenRequest = new TokenRequest(org.apache.commons.collections4.MapUtils.EMPTY_SORTED_MAP, client.getClientId(), clientDetails.getScope(), grantType);
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        Authentication authentication = authenticationManager.authenticate(new MobileAuthToken(mobile, deviceId, code));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
        OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
        oAuth2Authentication.setAuthenticated(true);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken wxMaToken(String header, String grantType, String code) {
        ClientsUtils.ClientInfo client = ClientsUtils.buildClientInfo(header);
        if (!StrUtil.equals(grantType, "wxma")) {
            throw new UnsupportedGrantTypeException("grant_type 必须为wxma");
        }
        ClientDetails clientDetails = ClientsUtils.getClientDetails(client.getClientId(), client.getClientSecret());
        TokenRequest tokenRequest = new TokenRequest(org.apache.commons.collections4.MapUtils.EMPTY_SORTED_MAP, client.getClientId(), clientDetails.getScope(), grantType);
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        Authentication authentication = authenticationManager.authenticate(new WxMaAuthToken(code));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
        OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
        oAuth2Authentication.setAuthenticated(true);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken wxMpToken(String header, String grantType, String code) {
        ClientsUtils.ClientInfo client = ClientsUtils.buildClientInfo(header);
        if (!StrUtil.equals(grantType, "wxmp")) {
            throw new UnsupportedGrantTypeException("grant_type 必须为wxmp");
        }
        ClientDetails clientDetails = ClientsUtils.getClientDetails(client.getClientId(), client.getClientSecret());
        TokenRequest tokenRequest = new TokenRequest(org.apache.commons.collections4.MapUtils.EMPTY_SORTED_MAP, client.getClientId(), clientDetails.getScope(), grantType);
        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
        Authentication authentication = authenticationManager.authenticate(new WxMpAuthToken(code, true));
        SecurityContextHolder.getContext().setAuthentication(authentication);
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
        OAuth2AccessToken oAuth2AccessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
        oAuth2Authentication.setAuthenticated(true);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken authorizationCodeToken(String clientId, String clientSecret, String code, String grantType, String redirectUri) {
        ClientDetails clientDetails = ClientsUtils.getClientDetails(clientId, clientSecret);
        Map<String, String> map = new HashMap<>();
        map.put("grant_type", "authorization_code");
        map.put("code", code);
        map.put("redirect_uri", redirectUri);
        TokenRequest tokenRequest = new TokenRequest(map, clientDetails.getClientId(), Collections.emptySet(), grantType);
        OAuth2RequestFactory requestFactory = new DefaultOAuth2RequestFactory(redisClientDetailsService);
        AuthorizationCodeTokenGranter authorizationCodeTokenGranter = new AuthorizationCodeTokenGranter(
                authorizationServerTokenServices, redisAuthorizationCodeServices, redisClientDetailsService, requestFactory);
        OAuth2AccessToken oAuth2AccessToken = authorizationCodeTokenGranter.grant("authorization_code", tokenRequest);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken clientToken(String header) {
        ClientsUtils.ClientInfo client = ClientsUtils.buildClientInfo(header);
        ClientDetails clientDetails = ClientsUtils.getClientDetails(client.getClientId(), client.getClientSecret());
        TokenRequest tokenRequest = new TokenRequest(org.apache.commons.collections4.MapUtils.EMPTY_SORTED_MAP, client.getClientId(), clientDetails.getScope(), "client_credentials");
        OAuth2RequestFactory requestFactory = new DefaultOAuth2RequestFactory(redisClientDetailsService);
        ClientCredentialsTokenGranter clientCredentialsTokenGranter = new ClientCredentialsTokenGranter(
                authorizationServerTokenServices, redisClientDetailsService, requestFactory);
        clientCredentialsTokenGranter.setAllowRefresh(true);
        OAuth2AccessToken oAuth2AccessToken = clientCredentialsTokenGranter.grant("client_credentials", tokenRequest);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken refreshToken(String token) {
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
        OAuth2Authentication auth = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication();
        ClientDetails clientDetails = redisClientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
        if (clientDetails == null) {
            throw new UnapprovedClientAuthenticationException("clientId对应的信息不存在");
        }
        OAuth2RequestFactory requestFactory = new DefaultOAuth2RequestFactory(redisClientDetailsService);
        RefreshTokenGranter refreshTokenGranter = new RefreshTokenGranter(authorizationServerTokenServices,
                redisClientDetailsService, requestFactory);
        Map<String, String> map = new HashMap<>();
        map.put("grant_type", "refresh_token");
        map.put("refresh_token", accessToken.getRefreshToken().getValue());
        TokenRequest tokenRequest = new TokenRequest(map, auth.getOAuth2Request().getClientId(),
                auth.getOAuth2Request().getScope(), "refresh_token");
        OAuth2AccessToken oAuth2AccessToken = refreshTokenGranter.grant("refresh_token", tokenRequest);
        tokenStore.removeAccessToken(accessToken);
        return oAuth2AccessToken;
    }

    @Override
    public OAuth2AccessToken getTokenInfo(String token) {
        if (StringUtils.isEmpty(token)) {
            throw new UnapprovedClientAuthenticationException("token 不能为空！");
        }
        return tokenStore.readAccessToken(token);

    }

    @Override
    public String removeToken(String token) {
        String message = "移除token失败！";
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
        if (accessToken != null) {
            tokenStore.removeAccessToken(accessToken);
            if (accessToken.getRefreshToken() != null) {
                tokenStore.removeRefreshToken(accessToken.getRefreshToken());
                message = "移除token成功！";
            }
        }
        return message;
    }

    public List<String> findKeysForPage(String patternKey, int pageNum, int pageSize) {

        try {
            Set<String> execute = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
                Set<String> binaryKeys = new HashSet<>();
                Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(patternKey).count(1000).build());
                int tmpIndex = 0;
                int startIndex = (pageNum - 1) * pageSize;
                int end = pageNum * pageSize;
                while (cursor.hasNext()) {
                    if (tmpIndex >= startIndex && tmpIndex < end) {
                        binaryKeys.add(new String(cursor.next()));
                        tmpIndex++;
                        continue;
                    }
                    // 获取到满足条件的数据后,就可以退出了
                    if (tmpIndex >= end) {
                        break;
                    }
                    tmpIndex++;
                    cursor.next();
                }
                connection.close();
                return binaryKeys;
            });

            List<String> result = new ArrayList<>(pageSize);
            result.addAll(execute);
            return result;
        } catch (Exception e) {
            throw new ServiceException(e.getMessage());
        }
    }

}
